unit FileSyncDeamon;

{$mode objfpc}{$H+}

//警告：使用fpc自带的daemonapp.inc注册服务会出现中文乱码，
//需修改daemonapp.inc使用WideString，方法见:说明.txt

interface

uses
  Classes, SysUtils, DaemonApp, IniFiles, FileSync, Windows, logger;

const
  SERVICE_DESC_SHORT = '云琛科技文件同步服务';
  BF_KEY = '40cA6f296F025bE70W0dBc6f08b7bP2HMnx';

type

  { TFileSyncDeamon }

  TFileSyncDeamon = class(TDaemon)
    procedure DataModuleAfterInstall(Sender: TCustomDaemon);
    procedure DataModuleBeforeUnInstall(Sender: TCustomDaemon);
    procedure DataModuleCreate(Sender: TObject);
    procedure DataModuleDestroy(Sender: TObject);
    procedure DataModuleStart(Sender: TCustomDaemon; var OK: Boolean);
    procedure DataModuleStop(Sender: TCustomDaemon; var OK: Boolean);
  private
    FLogger: TLogger;      //日志
    FThreads: TList;
    procedure LoadConfig;
    procedure FreeThreads;
  public
    MaxLogFileSize: Cardinal;//日志文件上限
    procedure WriteLog(Text: string);
  end;

  function EncodePassword(Text: string): string;
  function DecodePassword(Text: string): string;
  function IIF(bool: boolean; Text1: string; Text2: string): string;

var
//  AFileSyncDeamon: TFileSyncDeamon;
  Critical, CriticalForZip: TRTLCriticalSection;
  //CPU占用率，有效值0-100（最低0，最高100）。倒过来变成Sleep时间。
  CPUUsage: Word;

implementation

uses BlowFish, base64;

//将密码加密为密文
function EncodePassword(Text: string): string;
var
    buf: TStringStream;
    enc: TBlowfishEncryptStream;
begin
    //Result := HMACSHA1Print(HMACSHA1Digest(AEC_KEY, Text));
    if Text = '' then Exit('');
    //文本长度必须为8的倍数
    if (Text.Length mod 8) <> 0 then begin
        Text += StringOfChar(' ', 8);
        SetLength(Text, (Text.Length div 8)*8);
    end;

    Result := '';
    enc := nil;
    try
        buf := TStringStream.Create('');
        enc := TBlowfishEncryptStream.Create(BF_KEY, buf);
        enc.Write(Text[1], Text.Length);
        Result := EncodeStringBase64(buf.DataString);
    finally
        if (enc <> nil ) then enc.Free;
        buf.Free;
    end;
end;

//还原密文为明文
function DecodePassword(Text: string): string;
var
    buf: TStringStream;
    dec: TBlowfishDecryptStream;
begin
    if (Text = '') then Exit('');

    Result := '';
    dec := nil;
    try
        buf := TStringStream.Create(DecodeStringBase64(Text));
        dec := TBlowfishDecryptStream.Create(BF_KEY, buf);
        SetLength(Result, buf.size);
        dec.Read(Result[1], buf.size);
        Result := Result.Trim();
    finally
        if (dec <> nil ) then dec.Free;
        buf.free;
    end;
end;

function IIF(bool: boolean; Text1: string; Text2: string): string;
begin
  if bool then
    result := Text1
  else
    result := Text2;
end;

procedure RegisterDaemon;
begin
  RegisterDaemonClass(TFileSyncDeamon)
end;

{$R *.lfm}

{ TFileSyncDeamon }

procedure TFileSyncDeamon.DataModuleAfterInstall(Sender: TCustomDaemon);
begin
    //WriteLog(Sender.Definition.DisplayName + '安装成功。');
    WriteLog(SERVICE_DESC_SHORT + '安装成功。');
    WriteLn(SERVICE_DESC_SHORT + '安装成功。');
end;

procedure TFileSyncDeamon.DataModuleBeforeUnInstall(Sender: TCustomDaemon);
begin
    //WriteLog(Sender.Definition.DisplayName + '已卸载。');
    WriteLog(SERVICE_DESC_SHORT + '已卸载。');
    WriteLn(SERVICE_DESC_SHORT + '已卸载。');
end;

procedure TFileSyncDeamon.DataModuleCreate(Sender: TObject);
begin
    //InitializeCriticalSection(Critical);
    //创建日志文件
    FLogger := TLogger.Create();
end;

procedure TFileSyncDeamon.DataModuleDestroy(Sender: TObject);
begin
    if Assigned(FThreads) and (FThreads.Count > 0) then begin
        FreeThreads;
        WriteLog(SERVICE_DESC_SHORT + '退出。');
    end;

    FLogger.Free;
    //CPUUsage设定为0时，启用临界区
    if 100 = CPUUsage then begin
        DeleteCriticalSection(Critical);
        DeleteCriticalSection(CriticalForZip);
    end;
end;

procedure TFileSyncDeamon.DataModuleStart(Sender: TCustomDaemon; var OK: Boolean);
begin
    OK := true;
    try
        WriteLog(SERVICE_DESC_SHORT + '正在启动...');
        FThreads := TList.Create;
        LoadConfig;
    except on Ex: Exception do begin
        OK := false;
        WriteLog('启动操作发生异常：' + ex.ToString());
        {BackTraceStrFunc(ExceptAddr), ExceptFrameCount, ExceptFrames}
        //raise;
    end; end;
end;

procedure TFileSyncDeamon.DataModuleStop(Sender: TCustomDaemon; var OK: Boolean);
begin
    OK := true;
    try
        WriteLog('接收到停止服务的请求，正在回收资源...');
        FreeThreads;
        WriteLog(SERVICE_DESC_SHORT + '已停止。');
    except on Ex: Exception do begin
        OK := false;
        WriteLog('停止操作发生异常：' + ex.ToString());
        //raise;
    end; end;
end;

procedure TFileSyncDeamon.LoadConfig;
label
    _loop;
var
    IniFile: TIniFile;
    i, n: Integer;
    SrcDir, DestDir, AWorkTime, section, Igonre: string;
    arr: TStringArray;
    pass: boolean;
    Config: TSyncConfig;
    Sync: TFileSyncThread;
    dwCode: DWORD;
    //procedure FormatPath(var path: string);
    //var
    //  c: integer;
    //begin
    //  for c := 1 to path.Length do
    //    if path[c] = '/' then
    //       path[c] := '\';
    //  if (path.Length > 0) and (path[path.Length] <> '\') then
    //     path += '\';
    //end;
begin
    Config := TSyncConfig.Create;
    IniFile := TIniFile.Create(ExtractFilePath(ParamStr(0)) + 'setting.ini');
    try
        Self.MaxLogFileSize := IniFile.ReadInteger('DEFAULT', 'MaxLogSize', MAXLOGFILESIZE div (1024*1024))*1024*1024;
        FLogger.MaxFileSize := Self.MaxLogFileSize;
        //CPU占用率，有效值0-100（最低0，最高100）。倒过来变成Sleep时间
        CPUUsage := 100 - Min(100, Max(0, IniFile.ReadInteger('DEFAULT', 'CPUUsage', 99)));
        //CPUUsage设定为0时，启用临界区
        if (100 = CPUUsage) then begin
            InitializeCriticalSection(Critical);
            InitializeCriticalSection(CriticalForZip);
        end;

        for i := 1 to 30 do begin
            //WriteLog(Format('加载%d。', [i]));
            section := i.ToString();
            if not IniFile.SectionExists(section) then goto _loop;

            //检查参数
            //SrcDir
            SrcDir := IniFile.ReadString(section, 'SrcDir', '').Trim();
            DecodeUncAccount(SrcDir, Config.SrcUncAccount);
            //如果带账号密码，提取正常路径
            if (SrcDir.IndexOf('@') > -1) then begin
                SrcDir := SrcDir.Substring(SrcDir.IndexOf('@') + 1).Trim();
            end;
            //TCustomDaemon有个函数叫Continue T_T ----------------------------------
            //if SrcDir = '' then Continue;
            if SrcDir = '' then goto _loop;
            SrcDir := IncludeTrailingPathDelimiter(SrcDir);
            //如果UNC路径，先连接
            if (strlen(Config.SrcUncAccount.Account) > 0) and (not ConnectUnc(SrcDir, Config.SrcUncAccount.Account, Config.SrcUncAccount.Password)) then begin
                dwCode := GetLastError();
                WriteLog(Format('配置%s：连接源目录失败：(%d)%s', [section, dwCode, SysErrorMessage(dwCode)]));
            end;
            if (not DirectoryExists(SrcDir)) then begin
                WriteLog(Format('配置%s：源目录%s不存在！', [section, SrcDir]));
                goto _loop;
            end;

            //DestDir
            DestDir := IniFile.ReadString(section, 'DestDir', '').Trim();
            DecodeUncAccount(DestDir, Config.DestUncAccount);
            //如果带账号密码，提取正常路径
            if (DestDir.IndexOf('@') > -1) then begin
                DestDir := DestDir.Substring(DestDir.IndexOf('@') + 1).Trim();
            end;
            if DestDir = '' then goto _loop;;
            DestDir := IncludeTrailingPathDelimiter(DestDir);
            //如果UNC路径，先连接
            if (strlen(Config.DestUncAccount.Account) > 0) and (not ConnectUnc(DestDir, Config.DestUncAccount.Account, Config.DestUncAccount.Password)) then begin
                dwCode := GetLastError();
                WriteLog(Format('配置%s：连接目标目录失败：(%d)%s', [section, dwCode, SysErrorMessage(dwCode)]));
            end;
            //如果目录不存在，也没有写入权，退出
            if (not IsUncDrive(DestDir)){unc目录默认不检查，只需同步时创建子目录} and (not DirectoryExists(DestDir)) and (not ForceDirectories(DestDir)) then begin
                WriteLog(Format('配置%s：目标目录创建失败：%s', [section, DestDir]));
                goto _loop;
            end;

            if (SrcDir = DestDir) then begin
                WriteLog('配置' + section +'：源目录与目标目录不能相同！');
                goto _loop;
            end;

            if (SrcDir.IndexOf(DestDir) > -1) or (DestDir.IndexOf(SrcDir)> - 1) then begin
                WriteLog('配置' + section +'：源目录与目标目录不能是父子目录的关系！');
                goto _loop;
            end;

            //Time
            AWorkTime := IniFile.ReadString(section, 'Time', '').Trim();
            if (AWorkTime <> '') and (
              (AWorkTime.IndexOf('-')=-1) or (AWorkTime.IndexOf(':') = -1)
            ) then begin
                WriteLog(Format('配置%s：工作时段%s错误！', [section, AWorkTime]));
                goto _loop;
            end;

            if (AWorkTime <> '') then begin
                arr := AWorkTime.Split(['-', ':']);
                if (Length(arr) <> 4) or (not TryStrToInt(arr[0], n))
                  or (not TryStrToInt(arr[1], n))
                  or (not TryStrToInt(arr[2], n))
                  or (not TryStrToInt(arr[3], n))
                then begin
                    WriteLog(Format('配置%s：工作时段%s错误！', [section, AWorkTime]));
                    goto _loop;
                end;

                if (StrToInt(arr[0]) * 100 + StrToInt(arr[1]) > StrToInt(arr[2]) * 100 + StrToInt(arr[3])) then begin
                    WriteLog(Format('配置%s：工作时段%s错误！', [section, AWorkTime]));
                    goto _loop;
                end;
            end;

            //Igonre
            Igonre := IniFile.ReadString(section, 'Igonre', '').Trim();
            if (Igonre <> '') then begin
                arr := Igonre.Split(',');
                pass := true;
                for n := 0 to Length(arr)-1 do begin
                  //全部忽略？
                  if (arr[n] = '*.*') then begin
                    WriteLog('配置' + section +'：使用通配符*.*将忽略所有文件，本任务无效');
                    pass := false;
                    break;
                  end;

                  //目录格式不正确
                  if (arr[n][1] = '[') and (arr[n][arr[n].length] <> ']')
                    or (arr[n][1] <> '[') and (arr[n][arr[n].length] = ']')
                  then begin
                    WriteLog(Format('配置%s：目录格式%s不正确！', [section, AWorkTime]));
                    pass := false;
                    break;
                  end;
                end;

                if (not pass) then goto _loop;
            end;

            Config.Section := section;
            Config.SrcDir := SrcDir;
            Config.DestDir := DestDir;
            Config.Recurse := IniFile.ReadInteger(section, 'Recurse', 0) <> 0;
            Config.Direction := TSyncDirection(IniFile.ReadInteger(section, 'Direction', 0));
            Config.WorkTime := AWorkTime;
            Config.Igonre := Igonre;
            Config.Compress := IniFile.ReadString(section, 'Compress', '').Trim().ToLower();
            Config.Password := IniFile.ReadString(section, 'Password', '').Trim();
            Config.Pause := Min(3600, Max(0, IniFile.ReadInteger(section, 'Pause', 5)));
            //只支持7z和zip两种格式，只有启用压缩时才可定义密码
            if (Config.Compress <> '7z') and (Config.Compress <> 'zip') then begin
                Config.Compress := '';
                Config.Password := '';
            end
            else if Config.Password <> '' then
                //有设定密码的，先解密
                Config.Password := DecodePassword(Config.Password);

            //创建同步线程
            //第一个线程只处理当天的文件
            Sync := TFileSyncThread.Create(Self, Config, true);
            FThreads.Add(Pointer(Sync));
            //第二个线程处理之前的文件
            Config.Section += 'B';
            Sync := TFileSyncThread.Create(Self, Config, false);
            FThreads.Add(Pointer(Sync));
            WriteLog(Format('读取配置%d：%s %s %s，%s。'
              , [i, Config.SrcDir
              , TSyncDirectionSymbol[integer(Config.Direction)]
              , Config.DestDir
              , IIF(Config.Compress <> '', '压缩为' + Config.Compress + IIF(Config.Password = '', '', '(带密码)'), '不压缩')]));

_loop:
        end;
        if 0 = FThreads.Count then
            WriteLog('未配置有效的作业，修改setting.ini后请重启服务以加载参数！')
        else
            WriteLog(Format('已启动%d个作业。', [FThreads.Count]));
    finally
        IniFile.Free;
        Config.Free;
    end;
end;

procedure TFileSyncDeamon.FreeThreads;
  function IsThreadFreed(const AThread: TThread): Boolean;
  var
     lpExitCode: LPDWORD;
  begin
    if not Assigned(AThread) then Exit(true);
    Result := not GetExitCodeThread(AThread.Handle, lpExitCode);
  end;
var
    i: Integer;
begin
    for i := FThreads.Count - 1 downto 0 do
        if not IsThreadFreed(TFileSyncThread(FThreads.Items[i])) then begin
            TFileSyncThread(FThreads.Items[i]).Terminate;
            //TFileSyncThread(FThreads.Items[i]).WaitFor;
            //TFileSyncThread(FThreads.Items[i]).Free;
        end;
    FreeAndNil(FThreads);
end;

procedure TFileSyncDeamon.WriteLog(Text: string);
begin
    //EnterCriticalSection(Critical);
    FLogger.WriteLog(Text);
    //LeaveCriticalSection(Critical);
end;

initialization
  RegisterDaemon;
end.

